Android 游戏开发工具大升级
不同的硬件厂商为 Android 用户带来了不同尺寸和体验的设备,因此,我们也一直努力地帮助开发者们将游戏呈现到尽多的 Android 设备并使得开发过程更加高效轻松。本文将向您介绍众多新的 Android 游戏开发工具以及游戏调试、打包和分发技巧,如果您更喜欢通过视频了解本文内容,请点击下方:
△ Android 游戏开发工具大升级
Bilibili 视频链接
https://www.bilibili.com/video/BV1B34y1Y7SR/
更高效的游戏开发工具
工欲善其事,必先利其器,我们针对开发工具做出了大量优化。游戏引擎主要使用 C 和 C++ 语言编写,而大多数的 Android API 都被设计成由 Kotlin 这样的托管代码来调用。所以我们在 Android Studio 中实现了同时调试 C/C++ 代码和托管代码的能力,这样一来您就可以像下图那样在 Kotlin 及 C/C++ 代码中分别设置断点,然后在两种编程环境内分别跟踪执行情况。您甚至还可以在托管代码和原生 (native) 代码之间跳转观察执行情况。
△ 演示通过 Android Studio 同时调试托管代码和 C/C++
另一方面,Android Studio 可以通过自动代码补全功能来加快您编写代码的速度,也支持您快速插入 JNI 函数原型在两种不同环境之中编程。此外,构建过程中会使用 Cmake 和 Gradle,它们能让您更好地利用可移植 build。
对于那些在 Microsoft Windows 操作系统上用 Visual Studio 编写 C/C++ 跨平台游戏的开发者来说,我们在 2021 年 7 月推出的 Android 游戏开发套件 (Android Game Development Kit) 中提供了一个 Android 游戏开发扩展 (AGDE, Android Game Development Extension)。它能够进一步简化您的开发进程,并且使用一系列支持 C 和 C++ 的调试器及性能分析工具帮助您在 Visual Studio 环境中直接针对 Android 设备进行构建。并且 AGDE 可以很方便地与多种构建系统集成起来,还能非常方便地整合进您使用虚幻引擎的工作流,如此一来,您就不需要专门分别针对桌面设备、游戏主机、Android 设备各使用一套工具集和构建系统了。
配置 AGDE 的过程也非常简单。扩展安装完成后,请切换到您的 Visual Studio 项目,然后添加 Android APK 模板。您可以在工具栏访问到各种常用的 Android 开发工具,比如 SDK 和 NDK 管理器、虚拟设备管理器、设备文件管理器、Logcat 以及性能分析器等,如下图所示:
△ Visual Studio 中的 Android 工具栏
在项目中配置好 Android 构建目标后,您只需要像开发标准桌面 Visual Studio 目标那样操作就行了,所有关于构建、部署、调试的操作都没有差别。您可以很方便地在调试器中设置断点、切换到反汇编代码中查看寄存器和内存块中的值,还可以通过并行堆栈查看并发情况。
△ 使用 Visual Studio 调试 Android 构建目标
另外,您不仅可以在专门的 Logcat 面板搜索日志输出、按照类型过滤,还可以通过 AGDE 快速访问原本在 Android Studio 提供的独立 CPU、内存分析器,如下图右所示:
△ 可供搜索的 Logcat 面板 (左);
独立的 CPU 和内存分析器 (右)
我们为这些工具设计了新的界面,增强了若干功能并支持原生内存采样。Android Studio 和 Android 游戏开发扩展工具的结合,为您提供了一整套丰富的工具来高效地开发游戏。
Android 游戏开发套件
俗话说,好马配好鞍。只有这些趁手的工具其实是不够的,我们还需要将这些工具集成到 Android 的托管代码 API 中。为此,我们在 Android 游戏开发套件 (AGDK) 中提供了全新的 C/C++ 库供您使用。
GameActivity
GameActivity 几乎就是用原生方式实现 (C/C++) 的 Android 标准 activity。它可以很好地与 Jetpack 库以及各种 Android 界面库结合使用。这样您就可以完全使用 C/C++ 编写游戏循环,同时充分利用基于 Jetpack 构建的各种库。另外,GameActivity 会被渲染到 SurfaceView 中,因此您可以很轻松地混用 Android 界面元素,比如 WebView、MapView 和广告 SDK 等各种服务所需要用到的视图。这样一来,您只需要在托管代码中通过一个简单加载 C/C++ 游戏模块的类,就能完成对游戏循环逻辑的整合了。有关代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content">
△ 需要显示的视图
public class MyGameActivity extends GameActivity {
static {
// 加载您的游戏库
System.loadLibrary("game");
}
}
您需要修改 AndroidManifest.xml 文件中的 meta-data,用来告知 GameActivity 需要从哪个库开始执行游戏循环。
<activity android:name=".MyGameActivity" android:label="@string/app_name">
<meta-data android:name="android.app.lib_name" android:value="game"/>
GameActivity 为您提供了若干原生回调方法,它们与 Android 的生命周期事件是相匹配的,这些事件回调可以很方便地整合进您的游戏循环中,您可以查看 GameActivityCallbacks 的使用参考来了解更多信息:
下面是一段非常简单的代码,我们会向您展示一个基于 native_app_glue 库的案例。native_app_glue 库提供了一种异于寻常的执行模式。
void android_main(struct android_app *app) {
// 您的游戏引擎
NativeEngine *engine = new NativeEngine(app);
// 您的游戏循环
engine->GameLoop();
delete engine;
}
在这里,android_main() 函数将在一个区别于主线程的新线程中被调用。您可以从与线程关联的 ALooper 中获取到 Android 的生命周期事件,就像这样:
while(1) {
int events;
struct android_poll_source *source;
// 如果没有动画发生,阻塞进程直到捕获某个事件
while ((ALooper_pollAll(IsAnimating() ? 0: -1, Null, &events, (void **) &source)) >= 0) {
// 处理事件
if (source != NULL) {
source->process(mApp, source);
}
}
}
除了捕获生命周期事件,您还可以用这个方法监听文件描述符 (file descriptor)。
Frame Pacing API
帧数调步 (frame pacing) API 可以帮助开发者解决由较短游戏帧造成的拖延现象,以及避免较长游戏帧导致的拖延卡顿。如果设备支持选择刷新率功能,则可以为玩家提供更灵活和流畅的显示效果。
实现 GameActivity 后,您可以选择使用 OpenGL/ES 或者 Vulkan 将内容渲染到表层。不论您选择了哪种 API,Android Frame Pacing 库都会帮您妥善处理渲染过程,它会将游戏的逻辑、渲染循环与 Android 的显示子系统、底层显示相关的硬件进行同步,从而实现更流畅的渲染。
更多信息,请参阅 Frame Pacing 库:
Oboe API
△ Oboe API
一款制作精良的游戏离不开出色的音效,这也是 Oboe 音频库所提供的能力。您可以在 Android 4.1 及更高版本的系统上使用它的 API。在 API 级别 27 的设备上,Oboe 会通过 AAudio 在设备上尽可能协调软硬件而达到最低的音频延迟。而对于较低版本的设备,Oboe 会使用 OpenSL ES 来尽可能保证兼容性。
AAudio
https://developer.android.google.cn/ndk/guides/audio/aaudio/aaudio
游戏输入
Android 游戏开发套件还提供了两个可以与 GameActivity 互操作的库,分别用于处理游戏中的软键盘和手柄输入信号。
GameTextInput 在底层进行了很多复杂的工作,它能将 Android 系统的软键盘连接到您游戏内的文本编辑器上,还包括了显示和隐藏软键盘的操作。
如果您将这个库与 GameActivity 结合使用,那么无论有没有使用 native_app_glue,系统都能自动完成相关的配置。请看下面的代码,GameTextInput 库会将输入状态传递给您的游戏,这样您的文本编辑器就能正确反映 IME 的状态。
/**
* 获取最后收到的文本输入状态
*/
void GameActivity_getTextInputState(
GameActivity *activity,
GameTextInputGetStateCallback callback,
void* context);
△ 修改 AndroidManifest.xml 文件
GameTextInput 库
https://developer.android.google.cn/reference/games/game-text-input
另一种游戏中常见的输入设备是游戏手柄 (game controller)。您可以通过使用 Game Controller 库来充分发挥实体游戏手柄的作用。这个库会在手柄连接到设备或断开连接时向您的游戏发送通知,并且提供了关于按钮布局、方向轴和其他按键控制器元数据的信息。
Game Controller 库也在底层进行了封装,让您不需要复杂的实现也能无缝与各种各样的手柄轻松连接,甚至还接受鼠标作为输入设备。
支持大屏幕游戏
在智能电视畅玩游戏
// 您需要在多个 uses-feature 标签内声明这些权限:
android.hardware.touchscreen
android.hardware.faketouch
android.hardware.telephony
android.hardware.camera
android.hardware.nfc
android.hardware.location.gps
android.hardware.microphone
android.hardware.sensor
// 如果有必要的话,请添加 android.required="false"
您需要在清单中声明这些权限是非必要的,即 required="false"。这是由于很多您在 AndroidManifest 中提到的权限、功能都不是必须的,智能电视上往往不支持这些功能,比如触摸屏、摄像头、加速度传感器等。您可以通过添加如下代码,声明游戏支持使用遥控器作为手柄:
<uses-feature
android:name="android.hardware.gamepad"
android:required="false"/>
△ 声明游戏支持使用遥控器作为手柄
在需要时,可以声明 android:required="true",来表示在启动应用前电视需要先检查遥控器是否可用。
在 Chrome OS 运行
Chrome OS 如今已经成为了第二大桌面操作系统,并且拥有非常多的游戏玩家。它可以直接运行 Android 游戏,并内置了 Google Play 商店。玩家可以在运行 Chrome OS 的设备上畅玩支持横屏的游戏。大多数设备提供了触摸屏供玩家操作,对于某些没有触摸屏的设备,通常会通过鼠标或触控板来模拟触摸屏操作。当然,直接支持鼠标和键盘输入的游戏往往会给玩家带来更好的游戏体验。
△ 在 Chrome OS 上运行您的游戏
您可以在游戏中使用类似下面的代码捕捉鼠标或触控板事件:
fun onClick(view: View) {
view.requestPointerCapture()
}
override fun onCapturedPointerEvent(motionEvent: MotionEvent): Boolean {
// 捕获到的指针事件往往提供相对位置
val horizontalOffset: Float = motionEvent.x
val verticalOffset: Float = motionEvent.y
return true
}
△ 捕捉鼠标或触控板事件代码示例
val dm = resources.displayMetrics
// 用于精确缩放
val xdpi = dm.xdpi
val ydpi = dm.ydpi
// 用缩放因子计算 DPI
val densityDpi = dm.density * 160.0f
val scaledDensityDpi = dm.scaledDensity * 160.0f
您还可以使用 dm.density 和 dm.scaledDensity 获得近似的缩放系数。其中 1.0 对应着 DPI 值 160。ScaleDensity 用于根据用户偏好缩放 Android 应用的字体,所以,当您的游戏支持这一功能,必然会给用户带来更佳的体验。
有时出于性能考虑,您可能会想要设定一个显示密度的上限。如果您愿意,可以使用 surfaceHolder.setFixedSize 来自动且经济地缩放界面。这样不仅可以节省内存 (RAM),还可以减少需要着色的像素,并进一步节省电量和减少发热。具体参照下面这段代码:
var width = mSurfaceView.width
var height = mSurfaceView.height
val dm = resources.displayMetrics
if (dm.density > maxDensity) {
val newScaleFactor = maxDensity / dm.density
if (newScaleFactor != scaleFactor) {
width = (newScaleFactor * width).toInt()
height = (newScaleFactor * height).toInt()
mSurfaceView.holder.setFixedSize(width, height)
}
}
△ 使用 surfaceHolder.setFixedSize 缩放界面
您还可以使用 C/C++ 代码来自动实现缩放,比如下面的代码:
int32_t ret = ANativeWindow_setBuffersGeometry(
window, width, height, 0);
△ 使用 C/C++ 实现自动缩放
由于触控事件始终发生在屏幕坐标内,所以无论是用托管代码还是 C/C++ 实现,您都需要调整触控事件来匹配新的界面。
不同的游戏引擎有不同的后缓冲区缩放方式,如下图列举的 Unity、Unreal Engine、Godot 在这方面就有明显的区别。
externalNativeBuild {
cmake {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
处理更庞大的素材
使用 PAD 分发游戏资源
很多游戏都需要加载巨量的资源、素材文件,比如 3D 模型、贴图、音效、视频过场等等。Play Asset Delivery (PAD) 是一种针对 Android App Bundle 的扩展格式 。有了它,您可以向 Google Play 商店发布单个工件,而其中同时包含了游戏的代码部分和资源部分。
PAD 这种格式经过大量的改进和优化,能为您提供更高效的游戏分发体验。当您使用 PAD 交付游戏资源时,它会确保代码和资源的版本一致,从而让用户一打开游戏就能获得最新的二进制文件和资源,不再需要等待资源更新。并且 Google Play 的自动更新功能还可以帮您自动处理增量更新,使得用户可以在现有游戏版本上直接下载更新产生变化的部分,而不需要重新下载整个游戏。使用 PAD 的另一好处是内容下载的优化,也就是用户可以在首次运行游戏时加载必要的资源,随后需要新内容时又继续按需加载。
使用纹理压缩区分设备
使用设备类别划分
我们当前正努力实现按照设备类别进行定向分发的功能,这样一来您只需在每一套资源中分别提供不同类别所需要的资源文件即可。您可以根据 RAM 大小、设备型号等信息,以及物理屏幕大小等硬件特征进行划分,实现用更小的安装包覆盖更庞大的设备群体。
总结
感谢您的阅读,欢迎了解更多 Android 游戏开发的相关资源:
面向游戏开发者的官方文档与更多关于移动优化的指南
https://developer.android.google.cn/games
Android 游戏开发套件的指南、文档和参考 https://developer.android.google.cn/games/agdk
△ AGDK: 利用 Android 工具优化游戏
推荐阅读